<?php
/**
 * Copyright 2012  SURFfoundation
 * 
 * This file is part of ESCAPE.
 * 
 * ESCAPE is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * ESCAPE is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with ESCAPE.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * For more information:
 * http://escapesurf.wordpress.com/
 * http://www.surffoundation.nl/
 * 
 * Contact: d.vierkant@utwente.nl
 * 
 * @license http://www.gnu.org/licenses/gpl.html GNU GPLv3
 */
?>
<?php
require_once("config/escape.inc.php");
require_once("escape/include.php");

require_once("OAI/Server/Backend.php");
require_once("OAI/Server/Output.php");

class escape_repository_OaiServerBackend extends OAI_ServerBackend
{
	/**
	 * Repository instance.
	 * @var escape_repository_Repository
	 */
	protected $repository;
	
	protected $recordHeaders = null;
	
	var $options = array(
		'sets' => array(),
	);
	
	/**
	 * Construct a new OAI server backend object.
	 * @param array $options Backend configuration options
	 * @param escape_repository_Repository $repository ESCAPE repository
	 */
	public function __construct($options, &$repository)
	{
		parent::__construct();
		
		// assign user-defined options
		foreach($options as $key => $val){
			$this->options[$key] = $val;
		}
		
		$this->repository =& $repository;
	}
	
	/**
	 * Convert an aggregation to Dublin Core.
	 * @param escape_repository_OreAggregation $aggregation aggregation
	 * @return string Dublin Core metadata
	 */
	protected function convertAggregationToDc($aggregation)
	{
		$metadataRecord['dc_identifier'] = $this->_masqueradeIdentifier($aggregation->getId());
		$metadataRecord['dc_title'] = $aggregation->getTitle();
		
		$tpl = new OAI_ServerOutput;
		$tpl_file = 'Default/metadata_oai_dc.tpl';
		
		return $tpl->metadata($metadataRecord, $tpl_file);
	}

	/**
	 * Get the metadata for an aggregation in the requested metadata format.
	 * @param escape_repository_OreAggregation $aggregation aggregation
	 * @param string $format metadata format
	 * @return string Dublin Core metadata
	 */
	protected function getAggregationMetadata($aggregation, $format)
	{
		switch($format) {
			case 'oai_dc':
				return $this->convertAggregationToDc($aggregation);
			case 'oai_ore':
				return $aggregation->generateResourceMapDocument()->getRdfXml(true);
		}
	}

	/**
	 * Get a record array for an aggregation in the requested metadata format.
	 * @param string $aggregationId aggregation ID
	 * @param string $format metadata format
	 * @return array record array
	 */
	protected function getAggregationRecord($aggregationId, $format)
	{
		$aggregationUri = $this->repository->convertDataObjectIdToObjectUri($aggregationId);
		
		// get the resource
		try {
			$aggregation = $this->repository->getOreAggregationByUri($aggregationUri, null, false, true);
		
			$remDocument = $aggregation->generateResourceMapDocument();
			$deleted = $aggregation->isDeleted();
			$rdfResource = $aggregation->getProperties();
			
			$record = array();
			$record['identifier'] = $this->_masqueradeIdentifier($aggregationId);
			$record['deleted'] = $deleted;
			$record['datestamp'] = $aggregation->getLastModifiedDate();
			// only add metadata if the object isn't deleted
			if(!$deleted)
			{
				$record['metadata'] = $this->getAggregationMetadata($aggregation, $format);
			}
			// determine set membership
			$record['set_specs'] = array();
			foreach($this->options['sets'] as $setSpec => $setDefinition)
			{
				if($rdfResource->getFirstObject($setDefinition['predicate']) !== null)
				{
					$record['set_specs'][] = $setSpec;
				}
			}
			
			return $record;
		}
		catch(Exception $exception)
		{
			return false;
		}
	}
	
	/**
	* Method for verb "GetRecord".
	*
	* This verb is used to retrieve an individual metadata record from a repository.
	* Required arguments specify the identifier of the item from which the record is 
	* requested and the format of the metadata that should be included in the record.
	* Depending on the level at which a repository tracks deletions, a header with a 
	* "deleted" value for the status attribute may be returned, in case the metadata 
	* format specified by the metadataPrefix is no longer available from the repository
	* or from the specified item.
	*
	* @access public
	*/
	function getRecord($identifier, $format = 'oai_dc') 
	{
		$aggregationId = $this->_stripIdentifier($identifier);
		
		$record = $this->getAggregationRecord($aggregationId, $format);
		if(!$record)
		{
			return false;
		}
		
		$tpl = new OAI_ServerOutput;
		return $tpl->record($tpl->header($record));
	}
	
	/**
	 * Get record headers match the date range and set specified.
	 * 
	 * Warning: this method caches the result in an instance variable and returns the 
	 * cache next time it's called, regardless of the parameters specified. This 
	 * should never be a problem though as the OAI protocol always has one set of 
	 * parameters per request.
	 * 
	 * @param string $from date from
	 * @param string $until date until
	 * @param string $set set
	 * @return array record headers
	 */
	protected function getRecordHeaders($from = null, $until = null, $set = null)
	{
		if($this->recordHeaders === null)
		{
			// create a list of variables for set membership bindings
			$setVariables = '';
			foreach($this->options['sets'] as $setSpec => $setDefinition)
			{
				$setVariables .= ' ?set_' . $setSpec;
			}
			
			$sparqlQuery  = "select ?uri ?lastModifiedDate ?state" . $setVariables . "\n";
			$sparqlQuery .= "where\n";
			$sparqlQuery .= "{\n";
			$sparqlQuery .= "	?uri <info:fedora/fedora-system:def/model#hasModel> <info:fedora/" . $this->repository->config['system_object_namespace'] . ":cmodel.ore.Aggregation> .\n";
			$sparqlQuery .= "	?uri <info:fedora/fedora-system:def/view#lastModifiedDate> ?lastModifiedDate .\n";
			$sparqlQuery .= "	?uri <info:fedora/fedora-system:def/model#state> ?state .\n";
			// filter date ranges
			if($from)
			{
				$sparqlQuery .= "	filter (?lastModifiedDate >= '" . $this->repository->tripleStore->sparqlEscapeLiteral($from) . "' ) \n";
			}
			if($until)
			{
				$sparqlQuery .= "	filter (?lastModifiedDate <= '" . $this->repository->tripleStore->sparqlEscapeLiteral($until) . "' ) \n";
			}
			// bind sets
			foreach($this->options['sets'] as $setSpec => $setDefinition)
			{
				$sparqlQuery .= "	";
				if($set !== $setSpec)
				{
					// set bindings are optional except for the binding of a set specified in the request
					$sparqlQuery .= "optional ";
				}
				$sparqlQuery .= "{ ?uri <" . $this->repository->tripleStore->sparqlEscapeUri($setDefinition['predicate']) . "> ?set_" . $setSpec . " } .\n";
			}
			$sparqlQuery .= "}\n";
			$sparqlQuery .= "order by ?uri\n";
	
			$sparqlResultSet = $this->repository->tripleStore->sparqlSelect($sparqlQuery);
			$results = $sparqlResultSet->getResults();
	
			$tpl = new OAI_ServerOutput;
			
			$this->recordHeaders = array();
			foreach($results as $result)
			{
				$uri = $result['uri']['value']; 
				$lastModifiedDate = $result['lastModifiedDate']['value']; 
				$state = $result['state']['value']; 
				
				$aggregationId = $this->repository->convertObjectUriToDataObjectId($uri);
				
				$record = array();
				$record['identifier'] = $this->_masqueradeIdentifier($aggregationId);
				$record['deleted'] = $state === 'info:fedora/fedora-system:def/model#Deleted';
				$record['datestamp'] = $lastModifiedDate;
				$record['set_specs'] = array();
				// determin set membership
				foreach($this->options['sets'] as $setSpec => $setDefinition)
				{
					$setValue = $result['set_' . $setSpec]['value'];
					if($setValue)
					{
						$record['set_specs'][] = $setSpec;
					}
				}
				
				$this->recordHeaders[] = $record;
			}
		}
	
		return $this->recordHeaders;
	}
	
	/**
	* Method for verb "ListIdentifiers".
	*
	* This verb is an abbreviated form of ListRecords, retrieving only headers rather
	* than records. Optional arguments permit selective harvesting of headers based on 
	* set membership and/or datestamp. Depending on the repository's support for 
	* deletions, a returned header may have a status attribute of "deleted" if a record
	* matching the arguments specified in the request has been deleted.
	*
	* @param $args array		The optional OAI arguments
	* @param $cursorStart int	Token cursor starting point
	* @param $cursorEnd int		Token cursor ending point
	* @access public
	*/
	function listIdentifiers($args, $cursorStart, $cursorEnd) 
	{
		$cursorStart = @intval($cursorStart);
		$cursorEnd = @intval($cursorEnd);
		
		$recordHeaders = $this->getRecordHeaders($args['from'], $args['until'], $args['set']);
		
		if($cursorEnd)
		{
			$recordHeaders = array_slice($recordHeaders, $cursorStart, $cursorEnd - $cursorStart);
		}
				
		$tpl = new OAI_ServerOutput;
		
		$response = '';
		foreach($recordHeaders as $recordHeader)
		{
			$response .= $tpl->header($recordHeader);
		}

		return $response;
	}
	
	/**
	* Method for verb "ListMetadataFormats".
	*
	* This verb is used to retrieve the metadata formats available from a repository.
	* An optional argument restricts the request to the formats available for a specific
	* item.
	*
	* @access public
	*/
	function listMetadataFormats($identifier = null) 
	{	
		if(strlen($identifier)){
			// check if we get a result for this record
			if (!$this->getRecord($identifier)) {
				return false;
			}
		}
		
		if(!is_array($this->oai['metadata_formats']) || !count($this->oai['metadata_formats'])){
			return false;
		}
		
		$tpl = new OAI_ServerOutput;
		
		return $tpl->ListMetadataFormats($this->oai['metadata_formats']);
	}
	
	/**
	* Method for verb "ListRecords".
	*
	* This verb is used to harvest records from a repository.  Optional arguments 
	* permit selective harvesting of records based on set membership and/or datestamp.
	* Depending on the repository's support for deletions, a returned header may have a 
	* status attribute of "deleted" if a record matching the arguments specified in the 
	* request has been deleted. No metadata will be present for records with deleted 
	* status.
	*
	* @param $args array		The optional OAI arguments
	* @param $cursorStart int	Token cursor starting point
	* @param $cursorEnd int		Token cursor ending point
	* @access public
	*/
	function listRecords($args, $cursorStart, $cursorEnd) 
	{
		$cursorStart = @intval($cursorStart);
		$cursorEnd = @intval($cursorEnd);
		
		$format = $args['metadataPrefix'];
		
		$recordHeaders = $this->getRecordHeaders($args['from'], $args['until'], $args['set']);
		
		if($cursorEnd)
		{
			$recordHeaders = array_slice($recordHeaders, $cursorStart, $cursorEnd - $cursorStart);
		}
				
		$tpl = new OAI_ServerOutput;
		
		$response = '';
		foreach($recordHeaders as $recordHeader)
		{
			$aggregationId = $this->_stripIdentifier($recordHeader['identifier']); 
			
			$record = $this->getAggregationRecord($aggregationId, $format);
			if(!$record)
			{
				// record not found, skip
				continue;
			}
			
			$response .= $tpl->record($tpl->header($record));
		}

		return $response;
	}
	
	/**
	* Method for verb "ListSets".
	*
	* This verb is used to retrieve the set structure of a repository, useful for 
	* selective harvesting.
	*
	* @access public
	*/
	function listSets() 
	{
		$tpl = new OAI_ServerOutput;
		$response = $tpl->ListSets($this->options['sets']);
		
		return $response;
	}

    /**
    * Method for verb "Identify".
    *
	* This verb is used to retrieve information about a repository.  Some of the 
	* information returned is required as part of the OAI-PMH.  Repositories may 
	* also employ the Identify verb to return additional descriptive information.
	*
    * Has to be overwritten by each storage class
    *
    * @access public
    */
    function identify() 
    {
    	// fetch the earliest date stamp
    	$sparqlQuery  = "select ?lastModifiedDate\n";
		$sparqlQuery .= "where\n";
		$sparqlQuery .= "{\n";
		$sparqlQuery .= "	?uri <info:fedora/fedora-system:def/model#hasModel> <info:fedora/" . $this->repository->config['system_object_namespace'] . ":cmodel.ore.Aggregation> .\n";
		$sparqlQuery .= "	?uri <info:fedora/fedora-system:def/view#lastModifiedDate> ?lastModifiedDate .\n";
		$sparqlQuery .= "}\n";
		$sparqlQuery .= "order by ?lastModifiedDate\n";

		$sparqlResultSet = $this->repository->tripleStore->sparqlSelect($sparqlQuery);
		$results = $sparqlResultSet->getResults();

    	$this->oai['earliest_datestamp'] = null;
		if(count($results) > 0)
		{
			$this->oai['earliest_datestamp'] = $results[0]['lastModifiedDate']['value']; 
		}
    	
		// return OAI_METHOD_NOT_SUPPORTED to trigger the default idenfify implementation
		return OAI_METHOD_NOT_SUPPORTED;
    }
    
	/**
	 * Return the total number of records matching the parameters specified.
	 * This method is used for generating resumption tokens.
	 * @param array $args OAI parameters
	 * @return int total number of records
	 */
	function completeListSize($args)
	{
		$from = $args['from'];
		$until = $args['until'];
		$set = $args['set'];
		
		$recordHeaders = $this->getRecordHeaders($from, $until, $set);
		
		return count($recordHeaders);
	}
}